package nachos.network;

import java.util.HashMap;
import java.util.Iterator;

import nachos.machine.Kernel;
import nachos.machine.Lib;
import nachos.machine.Machine;
import nachos.machine.MalformedPacketException;
import nachos.machine.Packet;
import nachos.threads.KThread;
import nachos.threads.Lock;
import nachos.threads.Semaphore;
import nachos.threads.ThreadedKernel;

public class NachosProtocol {
	
	
	/**
	 * A collection of message queues, one for each local port. A
	 * <tt>PostOffice</tt> interacts directly with the network hardware. Because
	 * of the network hardware, we are guaranteed that messages will never be
	 * corrupted, but they might get lost.
	 *
	 * <p>
	 * The post office uses a "postal worker" thread to wait for messages to arrive
	 * from the network and to place them in the appropriate queues. This cannot
	 * be done in the receive interrupt handler because each queue (implemented
	 * with a <tt>SynchList</tt>) is protected by a lock.
	 */
		    /**
	     * Allocate a new post office, using an array of <tt>SynchList</tt>s.
	     * Register the interrupt handlers with the network hardware and start the
	     * "postal worker" thread.
	     * 
	     * 
	     */
	public HashMap<String, Socket> sockets;
	private Semaphore messageReceived;	// V'd when a message can be dequeued
	private Semaphore messageSent;	// V'd when a message can be queued
	private Lock sendLock;
    private static final char dbgNet = 'n';
    private Lock socketsLock; 
    public boolean stop = false;
	
    public NachosProtocol() {
		messageReceived = new Semaphore(0);
		messageSent = new Semaphore(0);
		sendLock = new Lock();
		socketsLock = new Lock();
		sockets = new HashMap<String, Socket>();
		
		Runnable receiveHandler = new Runnable() {
		    public void run() { receiveInterrupt(); }
		};
		Runnable sendHandler = new Runnable() {
		    public void run() { sendInterrupt(); }
		};
		Machine.networkLink().setInterruptHandlers(receiveHandler,
							   sendHandler);

		KThread t = new KThread(new Runnable() {
			public void run() { packetDelivery(); }
		    });
		
		KThread resender = new KThread(new Runnable() {
			public void run() { resend(); }
		    });

		t.fork();
		resender.fork();
	}

	  /**
	  * Wait for incoming messages, and then put them in the correct box.
	  */
    private void packetDelivery() {
    	//Lib.debug(dbgNet, "Starting packet delivery....");
		while (true) {
		    messageReceived.P();

		   // Lib.debug(dbgNet, "Message received....");
		    Packet p = Machine.networkLink().receive();

		    NachosPacket packet;

		    try {
		    	packet = new NachosPacket(p);
		    }
		    catch (MalformedPacketException e) {
			continue;
		    }
		    
		    Lib.debug(dbgNet, "delivering packet to port " + packet.dstPort
					   + ": " + packet);
		  
		    String key = getKey(p.srcLink, packet.srcPort, p.dstLink, packet.dstPort);
		    /// code here to check the flags of the packet and to process it depending
		    /// on the flags:
		    switch(packet.flag){
		    case NachosPacket.ACK:
		    	if(sockets.containsKey(key)){
		    		sockets.get(key).handleAck(packet.seqNo);
		    	}else{
		    		Lib.debug(dbgNet, "key: " + key + " not found, this connection does not exist");
		    	}
		    	break;
		    case NachosPacket.SYN_ACK:
		    	// need to pass on the packet, so the socket can get the info out of it
		    	if(sockets.containsKey(key)){
		    		sockets.get(key).putInReceiveBuffer(packet);
		    		sockets.get(key).waitForConnSignal.V();
		    	}else{
		    		Lib.debug(dbgNet, "key: " + key + " not found, this connection does not exist");
		    	}
		    	break;
		    case NachosPacket.SYN:
		      		Lib.debug(dbgNet, "Creating new socket");
		    		Socket socket = createSocket(p.srcLink, packet.srcPort, p.dstLink, packet.dstPort);
		    		socket.state = Socket.SYN_RCVD;
		    		socket.putInReceiveBuffer(packet);
		    		((NetKernel)Kernel.kernel).addToPending(socket);
		    		break;
		    case NachosPacket.FIN_ACK:
		    	if(sockets.containsKey(key)){
		    		socket = sockets.get(key);
		    		destroySocket(socket);
		    	}else{
		    		Lib.debug(dbgNet, "key: " + key + " not found, this connection does not exist");
		    	}
       			break;
		    case NachosPacket.STP:
		    	if(sockets.containsKey(key)){
		    		sockets.get(key).putInReceiveBuffer(packet);
		    		sockets.get(key).handleSTP(packet.seqNo);
		    	}else{
		    		Lib.debug(dbgNet, "key: " + key + " not found, this connection does not exist");
		    	}
		    	break;
		    case NachosPacket.FIN:
		    	if(sockets.containsKey(key)){
		    		sockets.get(key).putInReceiveBuffer(packet);
		    		sockets.get(key).handleFin(packet.seqNo);
		    	}else{
		    		Lib.debug(dbgNet, "key: " + key + " not found, this connection does not exist");
		    	}
		    	break;
		    case 0:	
		    	if(sockets.containsKey(key)){
		    		sockets.get(key).putInReceiveBuffer(packet);
		    		Lib.debug(dbgNet, "delivered packet: " + packet);
		    	}else{
		    		Lib.debug(dbgNet, "key: " + key + " not found, this connection does not exist");
		    	}
		    	
		    	break;
		    	
		    }
		}
    }

	    /**
	     * Called when a packet has arrived and can be dequeued from the network
	     * link.
	     */
    private void receiveInterrupt() {
    	messageReceived.V();
				
	}

	    /**
	     * Send a message to a box on a remote machine.
	     */
	public void send(NachosPacket packet) {
		if (Lib.test(dbgNet))
		    System.out.println("sending packet: " + packet);

		sendLock.acquire();

		Machine.networkLink().send(packet.packet);
		messageSent.P();

		sendLock.release();
	}

	    /**
	     * Called when a packet has been sent and another can be queued to the
	     * network link. Note that this is called even if the previous packet was
	     * dropped.
	     */
	 private void sendInterrupt() {
		messageSent.V();
	 }
      
	 public Socket createSocket(int dstLink, int dstPort, int srcLink, int srcPort){
	    Socket socket = new Socket(dstLink, dstPort, srcPort);
	   // ConnectionKey key = new ConnectionKey(dstLink, dstPort, srcLink, srcPort);
	    String key = getKey(dstLink, dstPort, srcLink, srcPort);
	    socketsLock.acquire();
	    sockets.put(key, socket);
	    Lib.debug(dbgNet, "Added socket with key: " + key);
	    socketsLock.release();
	    return socket;
	 }
	 
	 public void destroySocket(Socket socket){
		// ConnectionKey key = new ConnectionKey(dstLink, dstPort, srcLink, srcPort);	
		 socketsLock.acquire();
		 int port = -1;
		 if(socket!= null){
			 port = socket.srcPort;
			 String key = getKey(socket.dstHostID, socket.dstPort, socket.srcHostID, socket.srcPort);
			 Lib.debug(dbgNet, "destroying socket: " + key);
			 socket = sockets.remove(key);
		 }
		 socketsLock.release();
		 if(port > -1)
			 ((NetKernel) Kernel.kernel).freePorts.add(port);
		 
		 
	 }
	 
	 public String getKey(int dstLink, int dstPort, int srcLink, int srcPort){
		 return dstLink + ":" + dstPort + "_" + srcLink + ":" + srcPort;
	 }
	 
	 private void resend(){
			 while(!stop){
				 ThreadedKernel.alarm.waitUntil(200000);
				// first get the list of all the socket names - the keys to the hash table
				Iterator it = sockets.keySet().iterator();
				while(it.hasNext()){
					String key = (String) it.next();
					Socket socket = sockets.get(key);
					socket.sendBufferLock.acquire();
					//Lib.debug(dbgNet, "Socket: " + key + " buffer size: " +socket.sendBuffer.size() );
					int maxToSend = (socket.sendBuffer.size() <= Socket.windowSize) ? socket.sendBuffer.size() : Socket.windowSize;
					for(int i = 0; i < maxToSend; i++){
						NachosPacket rsndPacket = socket.sendBuffer.get(i);
						sendLock.acquire();
						Lib.debug(dbgNet, "Resending packet: " + rsndPacket);
						Machine.networkLink().send(rsndPacket.packet);
						messageSent.P();
						sendLock.release();
					}
					socket.sendBufferLock.release();
					
				}
			 }
		 }
	 
	 public void getExistingSocket(String key){
		 Socket socket = null;
		 socketsLock.acquire();
		 socket = sockets.get(key);
		 socketsLock.release();
		 
	 }
	 
	 private void sendSynAck(NachosPacket ack){
		  NachosPacket synAck = null;
		   try {
				synAck = new NachosPacket(ack.packet.srcLink, ack.srcPort,
								ack.packet.dstLink, ack.srcPort,
								new byte[0], ack.seqNo, NachosPacket.SYN_ACK);
				
			} catch (MalformedPacketException e) {
				Lib.debug(dbgNet, "Caught MalformedPacketException");
				//this really should not happen
			}  
			send(synAck);
	 }
	 

}
